/* Copyright 2009 Anthony Surma
This file is part of Java's Kademlia Extension (JKADX).

JKADX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

JKADX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have isConnected a copy of the GNU General Public License
along with JKADX.  If not, see <http://www.gnu.org/licenses/>.
*/

package jkadx.core;

import jkadx.networking.*;
import jkadx.dht.*;
import jkadx.xml.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @author Anthony Surma
 */
final class Core implements NetworkingInterface, DHTInterface
{
              final static int                              ALPHA = 3;
    private   final        int                              incomingPort;
    private   final        int                              outgoingPort;
    private   final        File                             contactsSaveFile;
    private   final        CoreConfig                       coreSettings;
    private   final        ArrayList<ElementNode>           settings;
    private   final        DHT                              dht;
    private   final        SocketController                 controller;
    private   final        ConcurrentHashMap<UniqueId, RPC> globalRpcHashMap;
    private   final        ReentrantLock                    check;
    private   final        UniqueId                         myUniqueId;
    private   volatile     Contact                          you;
    private   volatile     byte[]                           pingHeader;
    private   volatile     byte[]                           findNodeHeader;
    private   volatile     byte[]                           findValueHeader;
    private   volatile     byte[]                           storeHeader;

    @Testing  final        AtomicInteger isConnectedQueriesSent;
    @Testing  final        AtomicInteger isConnectedNotRepliedCount;

    @Testing  final        AtomicInteger                    queriesSent;
    @Testing  final        AtomicInteger                    queriesReceived;
    @Testing  final        AtomicInteger                    responsesSent;
    @Testing  final        AtomicInteger                    responsesReceived;
    @Testing  final        AtomicInteger                    notRepliedCount;

    @Testing  final        AtomicInteger                    findNodeQueriesSent;
    @Testing  final        AtomicInteger                    findNodeQueriesReceived;
    @Testing  final        AtomicInteger                    findNodeResponsesSent;
    @Testing  final        AtomicInteger                    findNodeResponsesReceived;

    @Testing  final        Object testLock = new Object();

    @Testing  final        String addIpAddress;
    @Testing  final        String addPort;
    @Testing  final        String addNodeId;

    Core(String settingsFilename, String contactsFilename, int inPort, int outPort,
            String addIpAddress, String addPort, String addNodeId)
            throws UnknownHostException, SocketException
    {              
        coreSettings      = new CoreConfig(settingsFilename, contactsFilename, inPort, outPort);
        settings          = coreSettings.getSettings();                
        you               = new Contact(myUniqueId = new UniqueId(ElementNode.getDataFor("thisNodeId", settings)),
                              InetAddress.getByName(ElementNode.getDataFor("externalIpAddress", settings)).getAddress(),
                              Integer.parseInt(ElementNode.getDataFor("incomingPort", settings)));                        
        dht               = new DHT(this, you.uniqueId);
        outgoingPort      = Integer.parseInt(ElementNode.getDataFor("outgoingPort", settings));
        incomingPort      = you.port;
        controller        = new SocketController(this, incomingPort, outgoingPort);
        contactsSaveFile  = new File(contactsFilename);
        globalRpcHashMap        = new ConcurrentHashMap<UniqueId, RPC>();
        queriesSent       = new AtomicInteger(0);

        findNodeQueriesSent       = new AtomicInteger(0);
        findNodeQueriesReceived   = new AtomicInteger(0);
        findNodeResponsesSent     = new AtomicInteger(0);
        findNodeResponsesReceived = new AtomicInteger(0);

        isConnectedQueriesSent       = new AtomicInteger(0);
        isConnectedNotRepliedCount   = new AtomicInteger(0);

        queriesReceived   = new AtomicInteger(0);
        responsesSent     = new AtomicInteger(0);
        responsesReceived = new AtomicInteger(0);
        notRepliedCount   = new AtomicInteger(0);
        check             = new ReentrantLock();



        // start with blank byte array of 67 bytes; bytes[] 0 -- 66
        byte[] bytes           = new byte[RPC.HEADER_LENGTH + UniqueId.NBITS / Byte.SIZE];
        // add my contact information to it (after the flag bits byte)
        you.insert(bytes, RPC.SOURCE_ADDRESS_OFFSET);
        // add the correct flag bits to each rpc
        bytes[0]        = RPC.PING_FLAG;
        pingHeader      = bytes;
        bytes           = bytes.clone();
        bytes[0]        = RPC.FIND_NODE_FLAG;
        findNodeHeader  = bytes;
        bytes           = bytes.clone();
        bytes[0]        = RPC.FIND_VALUE_FLAG;
        findValueHeader = bytes;
        bytes           = bytes.clone();
        bytes[0]        = RPC.STORE_FLAG;
        storeHeader     = bytes;
        
        this.addIpAddress = addIpAddress;
        this.addPort      = addPort;
        this.addNodeId    = addNodeId;
    }

    public static void main(String[] args)
    {
        Core    core             = null;
        String  settingsFilename = "settings";
        String  contactsFilename = "contacts";
        String  addIpAddress     = null;
        String  addPort          = null;
        String  addNodeId        = null;
        int     inPort           = -1;
        int     outPort          = -1;
        boolean settingsSet      = false;
        boolean contactsSet      = false;
        boolean cliSet           = false;
        boolean inPortSet        = false;
        boolean outPortSet       = false;
        boolean addSet           = false;

        if (args.length > 0)
        {
            final String USAGES = "\nUSAGES:" +
                                  "    java Core [--help | -help | /?] |\n" +
                                  "    java Core [-settings=<settings filename> |\n" +
                                  "               -contacts=<contacts filename> |\n" +
                                  "               -inport=<incoming udp port> |\n" +
                                  "               -outport=<outgoing udp port> | -cli]\n" +
                                  "               -add=<IpAddress>/<port>/<UniqueId>\n";
            
            for (String s : args)
            {
                if (s.startsWith("-settings=") && !settingsSet)
                {                    
                    settingsFilename = s.substring(10);
                    settingsSet = true;
                }
                else if (s.startsWith("-contacts=") && !contactsSet)
                {                   
                    contactsFilename = s.substring(10);
                    contactsSet = true;
                }
                else if (s.startsWith("-inport=") && !inPortSet)
                {
                    inPort = Integer.parseInt(s.substring(8));
                    inPortSet = true;
                }
                else if (s.startsWith("-outport=") && !outPortSet)
                {
                    outPort = Integer.parseInt(s.substring(9));
                    outPortSet = true;
                }
                else if (s.startsWith("-cli") && !cliSet)
                {
                    cliSet = true;
                }
                else if (s.startsWith("-add=") && !addSet)
                {
                    addSet = true;
                    String string = s.substring(5);
                    String stuff[] = string.split("/");
                    if (stuff.length == 3)
                    {
                        addIpAddress = stuff[0];
                        addPort = stuff[1];
                        addNodeId = stuff[2];
                    }

                }
                else
                {
                    System.out.println("\nInvalid option: " + s);
                    System.out.println(USAGES);
                    System.exit(1);
                }
            }                                        
        }        
        
        try
        {           
            if (cliSet)
            {
                Scanner in = new Scanner(System.in);
                System.out.println("\nJKADX Command Line Interface.  Type 'p' to Print menu.");
                while(true)
                {
                    String input = null;
                    System.out.print("> ");
                    input = in.nextLine();
                    if (input.equalsIgnoreCase("p"))
                    {
                        System.out.println("The following commands are case insensitive:\n");
                        System.out.println("(P)rint this menu");
                        if (core == null)
                        {
                            System.out.println("(L)oad Core");
                        }
                        else
                        {
                            System.out.println("(N)umber of known contacts");
                            System.out.println("(G)et contacts");
                            System.out.println("(T)est ports");
                            System.out.println("(J)oin ");
                            System.out.println("(A)bout this node");
                        }
                        System.out.println("(S)hutdown");
                        System.out.println();
                    }
                    else if (input.equalsIgnoreCase("S"))
                    {
                        //core.coreSettings.saveSettings();
                        return;
                    }
                    else if ((core == null) && input.equalsIgnoreCase("L"))
                    {
                        core = new Core(settingsFilename, contactsFilename, inPort, outPort, addIpAddress, addPort, addNodeId);
                        core.start();                        
                        System.out.println();

                    }
                    else if (core != null)
                    {
                        if (input.equalsIgnoreCase("J"))
                        {
                            core.join();
                            System.out.println();
                        }
                        else if (input.equalsIgnoreCase("N"))
                        {
                            System.out.println(core.dht.size() +
                                    " known contacts\n");
                        }
                        else if (input.equalsIgnoreCase("G"))
                        {
                            String s[] = (core.dht.toString()).split(DHT.SEPERATOR);
                            for (String temp : s)
                            {
                                System.out.println(temp);
                            }
                        }
                        else if (input.equalsIgnoreCase("A"))
                        {
                            System.out.println("Node Settings:\n");
                            for (ElementNode node : core.settings)
                            {
                                System.out.println("    " + node);
                            }
                            int[] stats = core.getStatistics();
                            System.out.println("\nSession Statistics:\n" +
                                               "\n    RPC queries sent......: " +
                                               stats[0] +
                                               "\n    RPC queries received..: " +
                                               stats[1] +
                                               "\n    RPC responses sent....: " +
                                               stats[2] +
                                               "\n    RPC responses received: " +
                                               stats[3] +
                                               "\n    RPCs now in flight....: " +
                                               core.globalRpcHashMap.size() +
                                               "\n    Not Responded Count...: " +
                                               stats[4] + "\n" +

                                               "\n    findNodeQueriesSent......: " +
                                               stats[5] +
                                               "\n    findNodeQueriesReceived..: " +
                                               stats[6] +
                                               "\n    findNodeResponsesSent....: " +
                                               stats[7] +
                                               "\n    findNodeResponsesReceived: " +
                                               stats[8] + "\n" +

                                               "\n    isConnectedQueriesSent....: " +
                                               stats[9] +
                                               "\n    isConnectedNotRepliedCount: " +
                                               stats[10]);

                        }
                        else if (input.equalsIgnoreCase("T"))
                        {
                            if (core.checkAndSetYourExternalAddress(core.you))
                            {
                                System.out.println("Everything is working.");
                            }
                            else
                            {
                                System.out.println("Detected network problem.");
                                System.out.println("If problem persists, check port forwarding on your router.");
                            }
                        }
                        else if (input.equalsIgnoreCase("R"))
                        {
                            core.getAndResetStatistics();
                        }

                    }
                }
            }
            else
            {
                core = new Core(settingsFilename, contactsFilename, inPort, outPort, addIpAddress, addPort, addNodeId);
                core.checkAndSetYourExternalAddress(core.you);
                core.start();
                core.join();
                if (settingsFilename != null) 
                {
                    System.out.println(settingsFilename + " will self-shutdown after 10 seconds.");
                    Thread.sleep(10000);
                    System.out.println(settingsFilename + " knew of " + core.dht.size() + " contact when it shutdown.");                    
                }
                else
                {
                    System.out.println(core + " will self-shutdown after 10 seconds.");
                    Thread.sleep(10000);
                    System.out.println(core + " knew of " + core.dht.size() + " contacts when it shutdown.");
                }
            }
        } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); }
        finally
        {
            if (cliSet && (core != null))
            {
                System.out.println("\nSent " + core.queriesSent + " queries...");
                System.out.println("and got " + core.responsesReceived + " responses back.\n");
                System.out.println("Received " + core.queriesReceived + " queries...");
                System.out.println("and sent " + core.responsesSent + " respones back.\n");
                System.out.print("Saving settings...  ");
            }

            if (cliSet)
            {
                if (core != null)
                {
                    core.coreSettings.saveSettings();
                    core.saveContacts();
                }
                System.out.println("Done.");
                System.out.print("Performing cleanup before exiting...  ");
            }
            if (core != null)
                core.cleanUp();

            if (cliSet)
                System.out.println("Done.");
        }
    }

    void join()
    {        
        int       position;
      
        if (dht.size() > 0) // if there's at least one contact in any kbucket
        {
            nodeLookup(myUniqueId);

            Contact[] c = dht.getKClosestContactsTo(myUniqueId);
            //System.out.println("c.length: " + c.length);
            position = UniqueId.getMSBAfterXoring(c[0].uniqueId, myUniqueId);
            //System.out.println("Position: " + position);
            while (--position >= 0)
            {           
                refresh(position);
            }
        }
        else
        {
            System.out.println("You have to add a contact from the command line first. " +
                    "Shutdown, and then use the -help switch.");
        }
    }

    void start()
    {
        int x;

	switch (x = loadContacts())
        {
            case -2:            
                System.out.println("FATAL ERROR: Cannot read settings file from disk.");
                System.exit(1);
                return;            
            case -1:            
                System.out.println("Contacts file does not exist.  If you've added your first contact from the command line, you need to shutdown to save it to a file.");
                firstTime();
                break;            
            case  0:            
                System.out.println("Empty contacts file found.");
                firstTime();
                break;            
            default:            
                System.out.println(x + " contacts successfully loaded from contacts file.");
                break;
        }
    }

    void firstTime()
    {
        
        if ((addIpAddress != null) && (addPort != null) && (addNodeId != null))
        {
          //  System.out.println("First Time: " + " \n" + addIpAddress + "/" + addPort + "\n" + addNodeId + "\n");
            try
            {
                this.dht.updateOrAddNew(new Contact(
                        new UniqueId(addNodeId), InetAddress.getByName(addIpAddress).getAddress(), Integer.parseInt(addPort)));
            } catch (UnknownHostException e) { throw new IllegalStateException(e.getMessage(), e); }
        }
       else
       {
            System.out.println("Cannot join network without any valid contacts.  You need to Shutdown, and enter a contact from the command line.  Use the --help switch for more info.\n");
       
       }
    }

    void refresh(int position)
    {
        nodeLookup(dht.getRandomUniqueIdFromKBucket(position));
    }

    void cleanUp()
    {
        this.controller.closeSockets();
    }

    @Override
    public void processIncoming(DatagramPacket packet)
    {
        byte[] payload = packet.getData();

        //System.out.println("Received thisPayload actual numCloser: " + packet.getLength());
        if ((payload[0] & RPC.APPLICATION_BIT_MASK) == RPC.APPLICATION_BIT_MASK)
        {
            try
            {
                // hard coded for now
                // obviously work needs to be done here, but for now, with only
                // one application... sending all of these to port 60000 should be
                // ok for now.

                // forwards packet
                if ((payload[0] & RPC.MPRIMEFINDER) == RPC.MPRIMEFINDER)
                {
                    controller.sendPacket(payload, InetAddress.getLocalHost(), 60001, packet.getLength());
                }
                else
                {
                    controller.sendPacket(payload, InetAddress.getLocalHost(), 60000, packet.getLength());
                }
                return;
            } catch (Exception e) {}
        }
        switch(payload[0] & RPC.FLAGS_BIT_MASK)
        {
            case 0: // dummy entry
                return;
            case RPC.PING_FLAG: //1                
                receivedPing(payload);               
                break;
            case RPC.FIND_NODE_FLAG: //2
                receivedFindNode(payload, packet.getLength());
                break;
            case 3: // dummy entry
                return;
            case RPC.FIND_VALUE_FLAG: //4
                receivedFindValue(payload);
                break;
            case 5: // dummy entry
            case 6: // dummy entry
            case 7: // dummy entry
                return;
            case RPC.STORE_FLAG: //8
                receivedStore(payload);
                break;            
            default:
                return;
        }
    }

    void receivedPing(final byte[] thisPayload)
    {
        UniqueId rpcId = new UniqueId(thisPayload, RPC.ID_OFFSET);
        RPC rpc = globalRpcHashMap.remove(rpcId);
        
        if (rpc != null )
        { // this is a reply
            rpc.gotPingReply();
          //  System.out.println("gotPingReply");
            responsesReceived.incrementAndGet();
            this.dht.updateOrAddNew(rpc.contact); // XXX for later
        }
        else
        { // this is a request
            UniqueId sourceUniqueId = new UniqueId(thisPayload, RPC.SOURCE_ID_OFFSET);
            
            if (!sourceUniqueId.matches(myUniqueId)) // prevents someone from tricking you into pinging yourself
            {
                Contact contact = new Contact(thisPayload, sourceUniqueId, RPC.SOURCE_ADDRESS_OFFSET);
                byte[] pingHeader = this.pingHeader;

                queriesReceived.incrementAndGet();
               
                for (int i = RPC.ID_OFFSET; i >= 0 ; --i)
                    thisPayload[i] = pingHeader[i];
                try
                {
                    controller.sendPacket(thisPayload, contact.getInetAddress(), contact.port);
                } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); }

                responsesSent.incrementAndGet();
                this.dht.updateOrAddNew(contact);
            }
        }
    }

    @NeedsTesting void receivedFindNode(byte[] thisPayload, int length)
    {
        UniqueId rpcId = new UniqueId(thisPayload, RPC.ID_OFFSET);
        RPC rpc = globalRpcHashMap.remove(rpcId);
        
        if (rpc != null )
        { // this is a reply
            // System.out.println("findNodeResponseReceived");
            findNodeResponsesReceived.incrementAndGet(); // for debugging
            responsesReceived.incrementAndGet(); // for debugging
            rpc.gotFindNodeReply(thisPayload, length, myUniqueId);
            this.dht.updateOrAddNew(rpc.contact);
        }
        else
        { // this is a request
           // System.out.println("findNodeRequestReceived");
            Contact[] closest = dht.getKClosestContactsTo(new UniqueId(thisPayload, RPC.FIND_NODE_TARGET_OFFSET));
            Contact   contact = new Contact(thisPayload, RPC.SOURCE_ADDRESS_OFFSET);            
            byte[]    payload = new byte[RPC.HEADER_LENGTH + (closest.length * Contact.BYTE_LENGTH)];
            byte[]    findNodeHeader = this.findNodeHeader;
            int       i;


            findNodeQueriesReceived.incrementAndGet();
            queriesReceived.incrementAndGet(); // for debugging            

            for (i = 0; i < RPC.ID_OFFSET; ++i)
                payload[i] = findNodeHeader[i];
            rpcId.insert(payload, RPC.ID_OFFSET);
            i = RPC.HEADER_LENGTH;    
            for (Contact c : closest)
                i = c.insert(payload, i);
            try
            {
                controller.sendPacket(payload, contact.getInetAddress(), contact.port);            
            } catch (IOException f) { throw new IllegalStateException(f.getMessage(), f); }

            findNodeResponsesSent.incrementAndGet();
            responsesSent.incrementAndGet();
            this.dht.updateOrAddNew(contact);
        }
    }

    @NeedsTesting Contact[] nodeLookup(final UniqueId target)
    {
        final Contact[]                      Initialcontacts = dht.getKClosestContactsTo(target);
        int                                  alphaMax        = (Initialcontacts.length > ALPHA) ? ALPHA : Initialcontacts.length; // get the number of contacts, up to ALPHA
        final ConcurrentHashMap<RPC, Object> rpcSentHashMap  = new ConcurrentHashMap<RPC, Object>();
        final ConcurrentSkipListSet<Contact> sortedList      = new ConcurrentSkipListSet<Contact>(new ContactByDistance(target));
        final HashMap<Contact, Object>       contactedHashMap    = new HashMap<Contact, Object>(60);             
              byte[]                         payload         = findNodeHeader.clone();              
              UniqueId                       rpcId;
              RPC                            rpc;              
              CountDownLatch                 cdlForThisRound = new CountDownLatch(1);


        final AtomicInteger                  totalAdded    = new AtomicInteger(Initialcontacts.length);
        final AtomicInteger                  totalReceived = new AtomicInteger(0);

        // add the K closest contacts this node knows about to the sortedlist
        for (int i = Initialcontacts.length - 1; i >= 0; i--)
        {
            sortedList.add(Initialcontacts[i]);
        }
                     
        for (int i = 0; i < alphaMax; ++i)
        {
            Contact c = Initialcontacts[i];
            rpcId = UniqueId.newRandom();
            rpcId.insert(payload, RPC.ID_OFFSET);
            rpc = new RPC(cdlForThisRound, c, sortedList, target, rpcSentHashMap, rpcId, totalAdded, totalReceived);
            globalRpcHashMap.put(rpcId, rpc);
            rpcSentHashMap.put(rpc, Boolean.TRUE);
            contactedHashMap.put(c, Boolean.TRUE);
            try
            {              
              //  System.out.println("rpcHashMap.containsKey: " + rpcHashMap.containsKey(rpcId));
              //  System.out.println("test.matches: " + new UniqueId(payload, RPC.ID_OFFSET).matches(rpcId));
             //    System.out.println("Sending findnode request to: " + c);
                controller.sendPacket(payload, c.getInetAddress(), c.port);
            } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); }            
            queriesSent.incrementAndGet();
            findNodeQueriesSent.incrementAndGet();            
        }
        try
        {

           cdlForThisRound.await(RPC.TIMEOUT_NS, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) { throw new IllegalStateException(e.getMessage(), e); }                                      

        nextNodeLookupRound(rpcSentHashMap, sortedList, contactedHashMap, target, totalAdded, totalReceived, 
                payload, globalRpcHashMap, controller, queriesSent, findNodeQueriesSent);
        // done.

        // Can do something with the contacts that didn't respond in the rpcList
        // For now I'll just remove them from the hashmap.

     
            // Can do something with the contacts that didn't respond in the rpcList
            // For now I'll just remove them from the hashmap.
          

        
        for (RPC r : rpcSentHashMap.keySet())
        {
            notRepliedCount.incrementAndGet();
            //globalRpcHashMap.remove(r.rpcId);
        }

        Iterator<Contact> iterator = sortedList.iterator();
        int num = (Initialcontacts.length == KBucket.K) ? KBucket.K : sortedList.size();
        num = (num  <= KBucket.K) ? num : KBucket.K;
        Contact[] result = new Contact[num];
        for (int i = 0; i < num; i++)
        {
            result[i] = iterator.next();
        }

       // System.out.println("result.length: " + result.length);

        return result;

    }

    //nextNodeLookupRound(rpcSentHashMap, sortedList, contactedHashMap, target, totalAdded, totalReceived);
    static void nextNodeLookupRound(ConcurrentHashMap<RPC, Object> rpcSentHashMap, ConcurrentSkipListSet<Contact> sortedList,
            HashMap<Contact, Object> contactedHashMap, UniqueId target, AtomicInteger totalAdded, AtomicInteger totalReceived,
            byte[] payload, ConcurrentHashMap<UniqueId, RPC> globalRpcHashMap, SocketController controller, AtomicInteger queriesSent, AtomicInteger findNodeQueriesSent)
    {        
        UniqueId          rpcId;
        RPC               rpc;
        CountDownLatch    cdlForThisRound;                
        Iterator<Contact> iterator;      
        int temp;
        int alphaMax = ((temp = (sortedList.size() - contactedHashMap.size())) > ALPHA) ? ALPHA : temp; // get the number of contacts not yet contaced, up to ALPHA
        Contact[] contactArray = new Contact[alphaMax];
     
        if ((alphaMax > 0) && (totalReceived.get() < KBucket.K)) // stop when received K number of replies
        {                      
            iterator       = sortedList.iterator();

            for (int i = 0; i < alphaMax ;)
            {
                Contact c = iterator.next();

                if (!contactedHashMap.containsKey(c))
                {
                    contactArray[i++] = c;                 
                }               
            }

            cdlForThisRound = new CountDownLatch(1);
            for (int i = 0; i < alphaMax; i++)
            {
                Contact c = contactArray[i];
                rpcId = UniqueId.newRandom();

                rpcId.insert(payload, RPC.ID_OFFSET);
                rpc = new RPC(cdlForThisRound, c, sortedList, target, rpcSentHashMap, rpcId, totalAdded, totalReceived);
                globalRpcHashMap.put(rpcId, rpc);
                rpcSentHashMap.put(rpc, Boolean.TRUE);
                contactedHashMap.put(c, Boolean.TRUE);
                try
                {
                    controller.sendPacket(payload, c.getInetAddress(), c.port);
                } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); }
                queriesSent.incrementAndGet();
                findNodeQueriesSent.incrementAndGet();
            }

            try
            {
                cdlForThisRound.await(RPC.TIMEOUT_NS, TimeUnit.NANOSECONDS);
            } catch (InterruptedException e) { throw new IllegalStateException(e.getMessage(), e); }
            nextNodeLookupRound(rpcSentHashMap, sortedList, contactedHashMap, target, totalAdded, totalReceived,
                payload, globalRpcHashMap, controller, queriesSent, findNodeQueriesSent);
        }
    }

    void receivedFindValue(byte[] payload)
    {

    }

    void receivedStore(byte[] payload)
    {

    }

    /**
     *
     * Checks if a thisContact is currently responding to requests.
     *
     * @param thisContact The Contact to check for connectivity.
     * @return Returns <code>true</code> if the thisContact can be detected within a
     * reasonable amount of time.  Otherwise, returns <code>false</code>.
     */ 
    @Override
    public boolean isConnected(final Contact contact)
    {
            byte[] payload = this.pingHeader.clone();
            CountDownLatch cdl = new CountDownLatch(1);
            UniqueId rpcId = UniqueId.newRandom();
            RPC rpc = new RPC(cdl, contact);
            boolean isConnected;

            rpcId.insert(payload, RPC.ID_OFFSET);
            globalRpcHashMap.put(rpcId, rpc);
            try
            {
                controller.sendPacket(payload, contact.getInetAddress(), contact.port);            
            } catch (IOException e) { throw new IllegalStateException(e.getMessage(), e); }
            isConnectedQueriesSent.incrementAndGet();
            queriesSent.incrementAndGet();
            try
            {
                isConnected = cdl.await(RPC.TIMEOUT_NS, TimeUnit.NANOSECONDS);
            } catch (InterruptedException e) { throw new IllegalStateException(e.getMessage(), e); }
            globalRpcHashMap.remove(rpcId);
            if (!isConnected)
            {
                isConnectedNotRepliedCount.incrementAndGet();
                notRepliedCount.incrementAndGet();
            }
            return isConnected;       
    }

    void saveContacts()
    {
	FileOutputStream fos;
	ObjectOutputStream oos;
	String dhtContacts = dht.toString();

        try
	{
            if (dhtContacts.length() > 0)
            {
		fos = new FileOutputStream(contactsSaveFile);
		oos = new ObjectOutputStream(fos);
		oos.writeUTF(dhtContacts);
		oos.flush();
		oos.close();
                fos.close();
	    }
        }
	catch (FileNotFoundException ex){}
	catch (IOException ex) {}
    }

    int loadContacts()
    {
        try
	{            
		final FileInputStream   fis          = new FileInputStream(contactsSaveFile);
		final ObjectInputStream ois          = new ObjectInputStream(fis);
		final String            akb          = ois.readUTF();
		final String[]          kbuckets;
		final int               id           = 0;
		final int               address      = 1;
		final int               port         = 2;
                      int               total        = 0;
                      String[]          contacts;

		ois.close();
		fis.close();
                kbuckets = akb.split(DHT.SEPERATOR);
		for (String k : kbuckets)
		{
		    contacts = k.split(KBucket.SEPERATOR);
                    for (String c : contacts)
                    {
                        //System.out.println(c);
                        String[]  s = c.split(Contact.SEPERATOR);
                        Contact old = new Contact(new UniqueId(s[id]),
                                      InetAddress.getByName(s[address]).getAddress(),
                                      Integer.parseInt(s[port]));
                        dht.updateOrAddNew(old);
                        total++;
                    }
		}		
                return total;
        }	
	catch (FileNotFoundException ex) {return -1;}
	catch (IOException ex) {return -2;}
    }

    boolean checkAndSetYourExternalAddress(Contact you)
    {
        
        if (!isConnected(you))
        {
            if (check.tryLock())
            {
                byte[] newValue;
                final String newExternalIpAddress = CoreConfig.discoverExternalIpAddress();

                coreSettings.setNewExternalIpAddress(newExternalIpAddress);
                try
                {
                    you = new Contact(new UniqueId(ElementNode.getDataFor(
                          "thisNodeId", settings)), InetAddress.getByName(
                          ElementNode.getDataFor("externalIpAddress", settings)).getAddress(),
                          Integer.parseInt(ElementNode.getDataFor("incomingPort", settings)));
                } catch (UnknownHostException e) { throw new IllegalStateException(e.getMessage(), e); }
                newValue    = this.pingHeader;
                you.insertAddress(newValue, RPC.SOURCE_ADDRESS_OFFSET);
                pingHeader = newValue;
                newValue    = newValue.clone();
                newValue[0] = RPC.FIND_NODE_FLAG;
                findNodeHeader = newValue;
                newValue    = newValue.clone();
                newValue[0] = RPC.FIND_VALUE_FLAG;
                findValueHeader = newValue;
                newValue    = newValue.clone();
                newValue[0] = RPC.STORE_FLAG;
                storeHeader = newValue;
                this.you = you;
                check.unlock();
            }
            else
            {
                check.lock(); 
                check.unlock();
            }
            return false;
        }        
        return true;
    }

    @Testing int[] getStatistics()
    {
        int[] result = new int[11];

        result[0] = queriesSent.get();
        result[1] = queriesReceived.get();
        result[2] = responsesSent.get();
        result[3] = responsesReceived.get();
        result[4] = notRepliedCount.get();

        result[5] = findNodeQueriesSent.get();
        result[6] = findNodeQueriesReceived.get();
        result[7] = findNodeResponsesSent.get();
        result[8] = findNodeResponsesReceived.get();

        result[9] = isConnectedQueriesSent.get();
        result[10] = isConnectedNotRepliedCount.get();
        return result;
    }

    @Testing int[] getAndResetStatistics()
    {
        int[] result = new int[11];

        result[0] = queriesSent.getAndSet(0);
        result[1] = queriesReceived.getAndSet(0);
        result[2] = responsesSent.getAndSet(0);
        result[3] = responsesReceived.getAndSet(0);

        result[4] = notRepliedCount.getAndSet(0);
        
        result[5] = findNodeQueriesSent.getAndSet(0);
        result[6] = findNodeQueriesReceived.getAndSet(0);
        result[7] = findNodeResponsesSent.getAndSet(0);
        result[8] = findNodeResponsesReceived.getAndSet(0);
        
        result[9] = isConnectedQueriesSent.getAndSet(0);
        result[10] = isConnectedNotRepliedCount.getAndSet(0);



        return result;
    }
}

// <editor-fold defaultstate="collapsed" desc="Annotation for code used only in tests.">
@interface Testing {
}// </editor-fold>

// <editor-fold defaultstate="collapsed" desc="Annotation for code that needs testing.">
@interface NeedsTesting {
}// </editor-fold>
